use crate::{ethvm::State as EvmState, ledger::Log};
use curl::easy::{Easy2, Handler, List, WriteError};
use ethereum_types::{Bloom, BloomInput};
use ethereum_types::{H160, H256, U256};
use ruc::*;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
    collections::{BTreeMap, HashMap},
    mem,
    result::Result as StdResult,
};
use web3_rpc_core::types::BlockNumber;

pub(crate) type BlockHeight = u64;

pub(crate) type HashValue = Vec<u8>;
pub(crate) type HashValueRef<'a> = &'a [u8];

pub(crate) type HashValueEvmFormat = H256;

pub(crate) type TmAddress = Vec<u8>;
pub(crate) type TmAddressRef<'a> = &'a [u8];

#[inline(always)]
pub fn sha2_sha256(bytes: &[&[u8]]) -> Vec<u8> {
    let mut hasher: Sha256 = Digest::new();
    for b in bytes {
        hasher.update(b);
    }
    hasher.finalize().as_slice().to_vec()
}

/// block proposer address of tendermint ==> evm coinbase address
#[inline(always)]
pub fn tm_proposer_to_evm_format(addr: TmAddressRef) -> H160 {
    const LEN: usize = H160::len_bytes();

    let mut buf = [0_u8; LEN];
    buf.copy_from_slice(&addr[..min!(LEN, addr.len())]);

    H160::from_slice(&buf)
}

/// block proposer address of tendermint ==> evm coinbase address
#[inline(always)]
pub fn hash_to_evm_format(hash: &HashValue) -> HashValueEvmFormat {
    const LEN: usize = H256::len_bytes();

    let mut buf = [0; LEN];
    buf.copy_from_slice(&hash[..min!(LEN, hash.len())]);

    H256::from_slice(&buf)
}

#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct InitialState {
    pub addr_to_amount: BTreeMap<H160, U256>,
}

pub fn block_number_to_height(
    bn: Option<BlockNumber>,
    evm_state: &EvmState,
) -> BlockHeight {
    let bn = if let Some(bn) = bn {
        bn
    } else {
        BlockNumber::Latest
    };

    match bn {
        BlockNumber::Hash {
            hash,
            require_canonical: _,
        } => block_hash_to_height(hash, evm_state),
        BlockNumber::Num(num) => num,
        BlockNumber::Latest => evm_state.block_hash_index.len() as u64,
        BlockNumber::Earliest => 1,
        BlockNumber::Pending => 0,
    }
}

#[inline(always)]
pub fn block_hash_to_height(hash: H256, evm_state: &EvmState) -> BlockHeight {
    evm_state
        .block_hash_index
        .get(hash.as_bytes().to_vec().as_ref())
        .unwrap_or(0)
}

#[inline(always)]
pub fn handle_bloom(b: &mut Bloom, logs: &[Log]) {
    for log in logs.iter() {
        b.accrue(BloomInput::Raw(&log.address[..]));
        for topic in &log.topics {
            b.accrue(BloomInput::Raw(&topic[..]));
        }
    }
}

#[inline(always)]
pub fn to_hex(data: impl AsRef<[u8]>) -> String {
    hex::encode(data.as_ref())
}

#[inline(always)]
pub fn from_hex(data: impl AsRef<[u8]>) -> Result<Vec<u8>> {
    hex::decode(data.as_ref()).c(d!())
}

/// Send a 'Http GET' request to the unix domain socket of tendermint RPC.
#[inline(always)]
pub fn tm_rpc_get(
    addr: &str,
    path: &str,
    params: &[&str], // i.e: &["a=b","c=d"]
    addr_is_unix_sock: bool,
) -> Result<(u32, Vec<u8>)> {
    #[derive(Debug, Serialize)]
    struct Body<'a> {
        id: u32,
        method: &'a str,
        params: HashMap<String, String>,
    }

    let method = path.trim_start_matches('/');
    let params = params
        .iter()
        .map(|p| {
            urlencoding::decode(p).c(d!()).map(|p| {
                let p = p.splitn(2, '=').fold(("", ""), |mut acc, new| {
                    acc.0 = acc.1;
                    acc.1 = new;
                    acc
                });
                (
                    p.0.to_owned(),
                    p.1.trim_matches(|c| '"' == c || '\'' == c).to_owned(),
                )
            })
        })
        .collect::<Result<HashMap<String, String>>>()?;

    let body = Body {
        id: 0,
        method,
        params,
    };

    let converted_post_body = serde_json::to_vec(&body).c(d!())?;
    tm_rpc_post(addr, converted_post_body, addr_is_unix_sock).c(d!())
}

/// Send a 'Http POST' request to the unix domain socket of tendermint RPC.
pub fn tm_rpc_post(
    addr: &str,
    req_body: Vec<u8>,
    addr_is_unix_sock: bool,
) -> Result<(u32, Vec<u8>)> {
    let mut easy = Easy2::new(Collector::default());

    easy.post(true).c(d!())?;
    easy.post_fields_copy(&req_body).c(d!())?;

    // NOTE: set headers after calling the `post` method.
    let mut headers = List::new();
    headers.append("Content-Type:application/json").c(d!())?;
    easy.http_headers(headers).c(d!())?;

    let addr = addr.trim();
    if addr_is_unix_sock {
        easy.unix_socket(addr).c(d!())?;
        easy.url("http://localhost/").c(d!())?;
    } else {
        easy.url(&format!("http://{}", addr)).c(d!())?;
    };
    easy.perform().c(d!(addr))?;

    let status_code = easy.response_code().c(d!())?;

    Ok((status_code, mem::take(&mut easy.get_mut().resp)))
}

#[derive(Default)]
struct Collector {
    resp: Vec<u8>,
}

impl Handler for Collector {
    fn write(&mut self, data: &[u8]) -> StdResult<usize, WriteError> {
        self.resp.extend_from_slice(data);
        Ok(data.len())
    }
}
